/*
 * Copyright (c) 2010 Vadim Zhukov <persgray@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */


#include "common.h"

#include <sys/param.h>
#include <sys/sysctl.h>
#include <sys/wait.h>

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>

/* For traversing processes */
#if defined(HAS_KVM_OPENFILES)
#	include <fnmatch.h>
#	include <kvm.h>
	/* Next is FreeBSD-only */
#	include <sys/user.h>
#elif defined(__linux__)
#	include <sys/stat.h>
#	include <dirent.h>
#	include <fcntl.h>
#	include <fnmatch.h>
#else
#define NO_PROC_WALKING
#endif

#include "strcase.h"
#include "logmsg.h"
#include "sensor.h"


enum { WD_MAX_EXEC_ARGS = 256 };


static void	watchdog_kill_by_name(struct wd_action *);
static void	watchdog_kill_by_pidfile(struct wd_action *);
static int	parse_signame(const char *);


int
parse_signame(const char *s) {
	int		 sig;
	const char	*errstr;

	if (isdigit(*s)) {
		sig = (int)strtonum(s, 1, _NSIG - 1, &errstr);
		if (errstr != NULL)
			sig = -1;
	} else {
		for (sig = 1; sig < _NSIG; sig++)
			if (!strncmp(s, sys_siglist[sig],
			    strlen(sys_siglist[sig])))
				break;
		if (sig == _NSIG)
			sig = -1;
	}
	if (sig == -1)
		errno = EINVAL;
	return sig;
}

struct watchdog *
watchdog_new(void) {
	struct watchdog *w;
	
	w = malloc(sizeof(struct watchdog));
	if (w == NULL)
		return NULL;
	SLIST_INIT(&w->conditions);
	SLIST_INIT(&w->actions);
	return w;
}

void
watchdog_free(struct watchdog *w) {
	struct wd_condition	*wdc;
	struct wd_action	*wda;

	while ((wda = SLIST_FIRST(&w->actions))) {
		SLIST_REMOVE_HEAD(&w->actions, entry);
		free(wda->s);
		free(wda);
	}
	while ((wdc = SLIST_FIRST(&w->conditions))) {
		SLIST_REMOVE_HEAD(&w->conditions, entry);
		free(wdc->s);
		free(wdc);
	}
}

int
watchdog_from_spec(struct watchdog *w, const char *spec) {
	struct wd_condition	*wdc;
	struct wd_action	*wda;
	int			 oerrno;
	const char		*errstr, *e, *e2, *p;

#define myerr(label, code, msg...) \
	do { \
		DPRINTFX(code, msg); \
		goto label; \
	} while(0)

	wdc = NULL;
	wda = NULL;
	e = strchr(spec, ',');
	if (e == NULL)
		myerr(ierr, 1, "no watchdog conditions or actions: %s", spec);
	if (e - spec >= sizeof(w->name))
		myerr(ierr, 1, "watchdog name too long: %s", spec);
	memcpy(w->name, spec, e - spec);
	w->name[e - spec] = '\0';
	spec = e + 1;
	for (p = e + 1; *p != '\0';) {
		if (strncmp(p, "if:", 3) == 0) {
			if (!SLIST_EMPTY(&w->actions))
				myerr(ierr, 1,
				    "error in watchdog spec here: %s", p);
			wdc = malloc(sizeof(struct wd_condition));
			if (wdc == NULL)
				myerr(err, 1, "malloc(wd_condition)");
			bzero(wdc, sizeof(struct wd_condition));
			p += 3;
			e = p + strcspn(p, "+:=,");
			if (*e != '+' || e == p)
				myerr(ierr, 1,
				    "error in watchdog spec here: %s", p);
			if (e - p >= sizeof(wdc->sens_name))
				myerr(ierr, 1, "sensor name too long: %s", p);
			memcpy(wdc->sens_name, p, e - p);
			w->name[e - p] = '\0';
			p = e + 1;
			e = p + strcspn(p, "+:=,");
			if (*e != '=' || e == p)
				myerr(ierr, 1,
				    "error in watchdog spec here: %s", p);
			
			if (e - p == 6 &&
			    strncmp(p, "NUM_EQ", 6) == 0)
				wdc->op = WD_CMP_NUM_EQ;
			else if (e - p == 6 &&
			    strncmp(p, "NUM_GE", 6) == 0)
				wdc->op = WD_CMP_NUM_GE;
			else if (e - p == 6 &&
			    strncmp(p, "NUM_GT", 6) == 0)
				wdc->op = WD_CMP_NUM_GT;
			else if (e - p == 6 &&
			    strncmp(p, "NUM_LE", 6) == 0)
				wdc->op = WD_CMP_NUM_LE;
			else if (e - p == 6 &&
			    strncmp(p, "NUM_LT", 6) == 0)
				wdc->op = WD_CMP_NUM_LT;
			else if (e - p == 6 &&
			    strncmp(p, "NUM_NE", 6) == 0)
				wdc->op = WD_CMP_NUM_NE;
			else if (e - p == 6 &&
			    strncmp(p, "NUM_IN", 6) == 0)
				wdc->op = WD_CMP_NUM_IN;
			else if (e - p == 7 &&
			    strncmp(p, "NUM_OUT", 7) == 0)
				wdc->op = WD_CMP_NUM_OUT;
			else if (e - p == 6 &&
			    strncmp(p, "STR_EQ", 6) == 0)
				wdc->op = WD_CMP_STR_EQ;
			else if (e - p == 9 &&
			    strncmp(p, "STR_EQ_NC", 9) == 0)
				wdc->op = WD_CMP_STR_EQ_NC;
			else if (e - p == 7 &&
			    strncmp(p, "STR_NEQ", 7) == 0)
				wdc->op = WD_CMP_STR_NEQ;
			else if (e - p == 10 &&
			    strncmp(p, "STR_NEQ_NC", 10) == 0)
				wdc->op = WD_CMP_STR_NEQ_NC;
			else if (e - p == 9 &&
			    strncmp(p, "STR_MATCH", 9) == 0)
				wdc->op = WD_CMP_STR_MATCH;
			else if (e - p == 12 &&
			    strncmp(p, "STR_MATCH_NC", 12) == 0)
				wdc->op = WD_CMP_STR_MATCH_NC;
			else if (e - p == 10 &&
			    strncmp(p, "STR_NMATCH", 10) == 0)
				wdc->op = WD_CMP_STR_NMATCH;
			else if (e - p == 13 &&
			    strncmp(p, "STR_NMATCH_NC", 13) == 0)
				wdc->op = WD_CMP_STR_NMATCH_NC;
			else
				myerr(ierr, 1,
				    "error in watchdog spec here: %s", p);
			p = e + 1;
			e = p + strcspn(p, "+:=,");
			switch (wdc->op) {
			case WD_CMP_NUM_EQ:
			case WD_CMP_NUM_GE:
			case WD_CMP_NUM_GT:
			case WD_CMP_NUM_LE:
			case WD_CMP_NUM_LT:
			case WD_CMP_NUM_NE:
				if (*e != ',' || e == p)
					myerr(ierr, 1,
					    "error in watchdog spec here: %s",
					    p);
				wdc->n = (int)strtonum(p, INT_MIN, INT_MAX,
				    &errstr);
				if (errstr != e)
					myerr(ierr, 1,
					    "error in watchdog spec here: %s",
					    p);
				break;

			case WD_CMP_NUM_IN:
			case WD_CMP_NUM_OUT:
				if (*e != '+' || e == p)
					myerr(ierr, 1,
					    "error in watchdog spec here: %s",
					    p);
				wdc->n = (int)strtonum(p, INT_MIN, INT_MAX,
				    &errstr);
				if (errstr != e)
					myerr(ierr, 1,
					    "error in watchdog spec here: %s",
					    p);
				p = e + 1;
				e = p + strcspn(p, "+:=,");
				if (*e != ',' || e == p)
					myerr(ierr, 1,
					    "error in watchdog spec here: %s",
					    p);
				wdc->n2 = (int)strtonum(p, INT_MIN, INT_MAX,
				    &errstr);
				if (errstr != e)
					myerr(ierr, 1,
					    "error in watchdog spec here: %s",
					    p);
				break;

			case WD_CMP_STR_EQ:
			case WD_CMP_STR_EQ_NC:
			case WD_CMP_STR_NEQ:
			case WD_CMP_STR_NEQ_NC:
			case WD_CMP_STR_MATCH:
			case WD_CMP_STR_MATCH_NC:
			case WD_CMP_STR_NMATCH:
			case WD_CMP_STR_NMATCH_NC:
				if (*e != ',' || e == p)
					myerr(ierr, 1,
					    "error in watchdog spec here: %s",
					    p);
				wdc->s = malloc(e - p + 1);
				if (wdc->s == NULL)
					myerr(err, 1,
					    "watchdog_from_spec: malloc");
				memcpy(wdc->s, p, e - p);
				wdc->s[e - p] = '\0';
			}
			SLIST_INSERT_HEAD(&w->conditions, wdc, entry);
			wdc = NULL;
		} else if (strncmp(p, "do:", 3) == 0) {
			if (SLIST_EMPTY(&w->conditions))
				myerr(ierr, 1,
				    "error in watchdog spec here: %s", p);
			wda = malloc(sizeof(struct wd_action));
			if (wda == NULL)
				myerr(err, 1, "malloc(wd_action)");
			bzero(wda, sizeof(struct wd_action));
			p += 3;
			e = p + strcspn(p, "+:=,");
			if (*e != '=' || e == p)
				myerr(ierr, 1,
				    "error in watchdog spec here: %s", p);

			if (e - p == 12 &&
			    strncmp(p, "KILL_PIDFILE", 12) == 0)
				wda->code = WD_ACTION_KILL_PIDFILE;
#ifndef NO_PROC_WALKING
			else if (e - p == 9 &&
			    strncmp(p, "KILL_NAME", 9) == 0)
				wda->code = WD_ACTION_KILL_NAME;
#endif
			else if (e - p == 7 &&
			    strncmp(p, "RUN_CMD", 7) == 0)
				wda->code = WD_ACTION_RUN_CMD;
			else
				myerr(ierr, 1,
				    "error in watchdog spec here: %s", p);
			p = e + 1;
			e = p + strcspn(p, "+:=,");
			if (*e != '=' || e == p)
				/* At least one parameter is needed */
				myerr(ierr, 1,
				    "error in watchdog spec here: %s", p);
			do {
				e2 = e + 1 + strcspn(e + 1, "+:=,");
				if (*e2 != '+' && *e2 != ',' && *e2 != '\0')
					myerr(ierr, 1,
					    "error in watchdog spec here: %s",
					    p);
				if (e - p != 3)
					myerr(ierr, 1,
					    "error in watchdog spec here: %s",
					    p);
				if (strncmp(p, "STR", 3) == 0) {
					wda->s = malloc(e2 - e);
					if (wda->s == NULL)
						myerr(err, 1,
						  "watchdog_from_spec: malloc");
					memcpy(wda->s, e + 1, e2 - e - 1);
					wda->s[e2 - e - 1] = '\0';
				} else if (strncmp(p, "SIG", 3) == 0) {
					switch (wda->code) {
					case WD_ACTION_KILL_NAME:
					case WD_ACTION_KILL_PIDFILE:
						break;

					default:
						myerr(ierr, 1, "error in "
						   "watchdog spec here: %s", p);
					}
					wda->sig = parse_signame(e + 1);
					if (wda->sig == -1)
						myerr(ierr, 1, "error in "
						   "watchdog spec here: %s", p);
				} else if (strncmp(p, "UID", 3) == 0) {
					switch (wda->code) {
					case WD_ACTION_KILL_NAME:
						break;

					default:
						myerr(ierr, 1, "error in "
						   "watchdog spec here: %s", p);
					}
					wda->uid = strtonum(e + 1, 0, LONG_MAX,
					    &errstr);
					if (errstr != e2)
						myerr(ierr, 1, "error in "
						   "watchdog spec here: %s", p);
				} else {
					myerr(ierr, 1, "error in watchdog "
					    "spec here: %s", p);
				}
				p = e2;
			} while (*e2 == '+');
			if (*p != '\0')
				p++;
			SLIST_INSERT_HEAD(&w->actions, wda, entry);
			wda = NULL;
		} else
			myerr(ierr, 1, "error in watchdog spec here: %s", p);
	}
	if (SLIST_EMPTY(&w->conditions))
		myerr(ierr, 1, "no watchdog conditions specified: %s", spec);
	if (SLIST_EMPTY(&w->actions))
		myerr(ierr, 1, "no watchdog actions specified: %s", spec);
	return 0;
#undef myerr

ierr:
	errno = EINVAL;

err:
	oerrno = errno;
	if (wda != NULL) {
		free(wda->s);
		free(wda);
	}
	if (wdc != NULL) {
		free(wdc->s);
		free(wdc);
	}
	errno = oerrno;
	return -1;
}

#ifndef NO_PROC_WALKING
void
watchdog_kill_by_name(struct wd_action *a) {
#ifdef HAS_KVM_OPENFILES
	struct kinfo_proc2	*plist;
	kvm_t			*kd;
	int			 i, nproc;
	char			 buf[_POSIX2_LINE_MAX], **argv;

/* Emulate this flag on FreeBSD */
#ifndef KVM_NO_FILES
#define KVM_NO_FILES 0
#endif

	kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, buf);
	if (kd == NULL) {
		DPRINTFX(0, "kvm_openfiles: %s", buf);
		return;
	}
	if (a->uid == (uid_t)-1)
		plist = kvm_getproc2(kd, KERN_PROC_ALL, 0,
		    sizeof(struct kinfo_proc2), &nproc);
	else
		plist = kvm_getproc2(kd, KERN_PROC_UID, a->uid,
		    sizeof(struct kinfo_proc2), &nproc);
	if (plist == NULL) {
		/* XXX: TODO: create and send error message (WTF I meant?!) */
		DPRINTFX(0, "kvm_getproc2: %s", kvm_geterr(kd));
		goto err;
	}

	for (i = 0; i < nproc; i++) {
		argv = kvm_getargv2(kd, &plist[i], 0);
		if (argv == NULL) {
			DPRINTFX(0, "kvm_getargv2: %s", kvm_geterr(kd));
			goto err;
		}
		if (fnmatch(a->s, argv[0], FNM_NOESCAPE) == 0)
#ifdef HAS_KVM_GETPROC2
			kill(plist[i].p_pid, a->sig);
#else
			kill(plist[i].ki_pid, a->sig);
#endif
	}

err:
	(void)kvm_close(kd);

#else /* Linux */
	struct dirent	 gde, *pde;
	struct stat	 stat;
	DIR		*gdd;
	int		 fd;
	pid_t		 pid;
	char		 tpath[48], cmd[PATH_MAX];
	const char	*errstr;

	fd = -1;
	gdd = opendir("/proc");
	if (gdd == NULL)
		goto err;
	while (readdir_r(gdd, &gde, &pde) != -1) {
		if (pde == NULL)
			goto ok;
		if (strchr("123456789", gde.d_name[0]) == NULL)
			/* Not a process entry */
			continue;
		if (snprintf(tpath, sizeof(tpath), "/proc/%s/cmdline",
		    gde.d_name) >= sizeof(tpath)) {
			DPRINTFX(1,
			    "watchdog_kill_by_name: too long direntry: %s",
			    gde.d_name);
			continue;
		}
		fd = open(tpath, O_RDONLY, 0);
		if (fd == -1) {
			DPRINTF(1, "watchdog_kill_by_name: open(%s)", tpath);
			continue;
		}
		if (a->uid >= 0) {
			if (fstat(fd, &stat) == -1) {
				DPRINTF(1, "watchdog_kill_by_name: fstat");
				goto try_next;
			}
			if (a->uid != stat.st_uid)
				goto try_next;
		}
		if (read(fd, cmd, sizeof(cmd)) > 0) {
			cmd[nitems(cmd) - 1] = '\0';    /* play safe */
			if (fnmatch(a->s, cmd, FNM_NOESCAPE) == 0) {
				/* XXX: How to detect min/max PID values? */
				pid = (pid_t)strtonum(gde.d_name, 0, INT_MAX,
				    &errstr);
				if (errstr == NULL && pid != getpid())
					kill(pid, a->sig);
			}
		}
try_next:
		(void)close(fd);
	}

err:
	DPRINTF(1, "watchdog_kill_by_name");
ok:
	if (fd != -1)
		(void)close(fd);
	if (gdd != NULL)
		(void)closedir(gdd);
#endif /* HAS_KVM_OPENFILES */
}
#endif /* NO_PROC_WALKING */

void
watchdog_kill_by_pidfile(struct wd_action *a) {
	FILE	*f;
	pid_t	 pid;
	int	 i;
	char	 s[12];

	f = fopen(a->s, "r");
	if (f == NULL) {
		DPRINTF(1, "watchdog_kill_by_name: fopen");
		return;
	}

	for (i = 0; fread(&s[i], 1, 1, f);) {
		if (!isdigit(s[i])) {
			DPRINTF(1, "watchdog_kill_by_name: not a PID file %s",
			    a->s);
			goto err;
		}
		if (isspace(s[i])) {
			if (i == 0)
				continue;
			s[i + 1] = '\0';
			pid = atoi(s);
			if (!pid) {
				DPRINTF(1, "watchdog_kill_by_name: "
				    "not a PID file %s", a->s);
				goto err;
			}
			kill(pid, a->sig);
			i = 0;
		} else if (++i == nitems(s) - 1) {
			DPRINTF(1, "watchdog_kill_by_name: not a PID file %s",
			    a->s);
			goto err;
		}
	}
	if (i != 0) {
		s[i + 1] = '\0';
		pid = atoi(s);
		if (pid)
			kill(pid, a->sig);
	}

err:
	(void)fclose(f);
}

void
watchdog_run_cmd(struct wd_action *a) {
	size_t	  i, len;
	pid_t	  pid;
	char	*args[WD_MAX_EXEC_ARGS], *p;

	pid = fork();
	if (pid == -1) {
		DPRINTF(0, "watchdog_check: fork");
		return;
	}
	if (pid != 0)
		return;

	bzero(args, sizeof(args));
	for (p = a->s, i = 0; (len = strlen(p)) != 0; p += len + 1, i++)
		args[i] = p;
	execvp(a->s, args);
	_exit(EXIT_FAILURE);
}

void
watchdog_check(const struct watchdog *w, const struct wsensor *s,
    struct log_msg *m) {
	struct wd_action	*a;
	struct wd_condition	*c;
#ifndef HAS_FNM_CASEFOLD
	char			*s1, *s2;
#endif
	time_t			 t;
	int			 n, match, misses;

	t = time(NULL);
	misses = 0;
	/* Always loop through all conditions because we track their duration */
	SLIST_FOREACH(c, &w->conditions, entry) {
		match = (fnmatch(c->sens_name, s->name,
		    FNM_NOESCAPE|FNM_PATHNAME) == FNM_NOMATCH);
		if (!match)
			goto after_matching;

		switch (c->op) {
		case WD_CMP_NUM_EQ:
			match = (atoi(m->text) == c->n);
			break;

		case WD_CMP_NUM_GE:
			match = (atoi(m->text) >= c->n);
			break;

		case WD_CMP_NUM_GT:
			match = (atoi(m->text) > c->n);
			break;

		case WD_CMP_NUM_LE:
			match = (atoi(m->text) <= c->n);
			break;

		case WD_CMP_NUM_LT:
			match = (atoi(m->text) < c->n);
			break;

		case WD_CMP_NUM_NE:
			match = (atoi(m->text) != c->n);
			break;

		case WD_CMP_NUM_IN:
			n = atoi(m->text);
			match = (n >= c->n && n <= c->n2);
			break;

		case WD_CMP_NUM_OUT:
			n = atoi(m->text);
			match = (n < c->n || n > c->n2);
			break;

		case WD_CMP_STR_EQ:
			match = (strcmp(c->s, m->text) == 0);
			break;

		case WD_CMP_STR_EQ_NC:
			match = (strcasecmp(c->s, m->text) == 0);
			break;

		case WD_CMP_STR_NEQ:
			match = (strcmp(c->s, m->text) != 0);
			break;

		case WD_CMP_STR_NEQ_NC:
			match = (strcasecmp(c->s, m->text) != 0);
			break;

		case WD_CMP_STR_MATCH:
			match = (fnmatch(c->s, m->text, FNM_NOESCAPE) == 0);
			break;

		case WD_CMP_STR_MATCH_NC:
#ifdef HAS_FNM_CASEFOLD
			match = (fnmatch(c->s, m->text,
			    FNM_NOESCAPE|FNM_CASEFOLD) == 0);
#else
			s1 = strduplcase(c->s);
			s2 = strduplcase(m->text);
			if (s1 != NULL && s2 != NULL)
				match = (fnmatch(c->s, m->text, FNM_NOESCAPE)
				    == 0);
			else {
				DPRINTF(1, "strduplcase");
				continue;
			}
			free(s2);
			free(s1);
#endif
			break;

		case WD_CMP_STR_NMATCH:
			match = (fnmatch(c->s, m->text, FNM_NOESCAPE) != 0);
			break;

		case WD_CMP_STR_NMATCH_NC:
#ifdef HAS_FNM_CASEFOLD
			match = (fnmatch(c->s, m->text,
			    FNM_NOESCAPE|FNM_CASEFOLD) != 0);
#else
			s1 = strduplcase(c->s);
			s2 = strduplcase(m->text);
			if (s1 != NULL && s2 != NULL)
				match = (fnmatch(c->s, m->text, FNM_NOESCAPE)
				    != 0);
			else {
				DPRINTF(1, "strduplcase");
				continue;
			}
			free(s2);
			free(s1);
#endif
			break;

		default:
			abort();	/* XXX */
		}

after_matching:
		if (match) {
			if (c->first == 0)
				c->first = t;
			if (t - c->first < c->minduration)
				misses = 1;
		} else {
			c->first = 0;
			misses = 1;
		}
	}

	if (!misses) {
		m->hdr->flags |= LOG_MSG_ACTION_ALARM;
		SLIST_FOREACH(a, &w->actions, entry) {
			switch (a->code) {
			case WD_ACTION_KILL_NAME:
#ifndef NO_PROC_WALKING
				/* Do not bail out if not supported */
				watchdog_kill_by_name(a);
#endif
				break;

			case WD_ACTION_KILL_PIDFILE:
				watchdog_kill_by_pidfile(a);
				break;

			case WD_ACTION_RUN_CMD:
				watchdog_run_cmd(a);
				break;

			default:
				abort();	/* XXX */
			}
		}
	}
	return;
}
